/*

This file is part of roomyak.

Copyright (C) 2008 Pixecur Technologies Incorporated (Esotera).
Visit Esotera at http://www.esoteras3.com
 
roomyak is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 3, or (at your option)
any later version.
 
roomyak is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
General Public License for more details.
 
You should have received a copy of the GNU General Public License
along with roomyak; see the file COPYING.  If not, write to 
Pixecur Technologies, 213-31 Peet Street, St. John's, NL, A1B 3W8. 
 
Linking this library statically or dynamically with other modules is
making a combined work based on this library.  Thus, the terms and
conditions of the GNU General Public License cover the whole
combination.
 
*/

package com.datasentinel.util.io;

import gnu.inet.mime.base64.Base64InputStream;
import gnu.inet.mime.base64.Base64OutputStream;

import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;

import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.CipherOutputStream;

import com.datasentinel.util.StringHelper;

/**
 * 
 * @author Areg Beketovski
 *
 */
public class StreamUtils
{
	protected static final Logger logger = Logger.getLogger(StreamUtils.class.getName());

	public static interface ProgressListener
	{
		void transfer(long totalBytesTransferred, double averageSpeed) throws IOException;
	}

	public static long copyDataWithBuffer(InputStream inputStream, OutputStream outputStream, byte[] copyBuffer)
			throws IOException
	{
		return copyDataWithBuffer(inputStream, outputStream, copyBuffer, false, null);
	}

	/**
	 * Copies all the data until EOF from inputStream to outputStream, using the provided copyBuffer. Reports progress if progressListener is not null.
	 * @return total number of bytes transferred
	 */
	public static long copyDataWithBuffer(InputStream inputStream, OutputStream outputStream, byte[] copyBuffer,
			boolean flush, ProgressListener progressListener) throws IOException
	{
		final long startTime = System.currentTimeMillis();

		double averageSpeed = 0;
		double prevSpeed = 0;

		long totalBytesRead = 0;
		boolean eof = false;
		do
		{
			int readByteCount = inputStream.read(copyBuffer);
			if (readByteCount == -1)
			{
				eof = true;
				break;
			}
			totalBytesRead += readByteCount;
			outputStream.write(copyBuffer, 0, readByteCount);

			if (flush)
				outputStream.flush();

			if (progressListener != null)
			{
				long currentTime = System.currentTimeMillis();
				double seconds = (((double)(currentTime - startTime)) / ((double)1000.0));
				long speed = (long)(((double)totalBytesRead) / ((double)seconds));
				averageSpeed = (((double)(averageSpeed + speed)) / 2.0);
				if (prevSpeed != averageSpeed)
				{
					if (logger.isLoggable(Level.FINER))
						logger.finer("Current speed: " + StringHelper.formatByteSize(averageSpeed) + "/sec");
					prevSpeed = averageSpeed;
				}
				progressListener.transfer(totalBytesRead, averageSpeed);
			}

		}
		while (!eof);
		return totalBytesRead;
	}


	public static void copyDataWithBuffer(long dataSize, InputStream inputStream, OutputStream outputStream,
			byte[] copyBuffer) throws IOException
	{
		exhaustiveCopyDataWithBuffer(dataSize, inputStream, dataSize, outputStream, copyBuffer);
	}

	public static interface DataListener
	{
		/**
		 * 
		 * @param copyStartOffset - how many bytes have been read before this point
		 * @param dataBuffer - a bunch of bytes read from the InputStream at this round
		 * @param length - what part of the dataBuffer are the actual data bytes (always starting from offset 0)
		 * 
		 * @return if returns true - transfer will stop
		 */
		boolean transfer(long copyStartOffset, byte[] dataBuffer, int length);
	}


	public static void readDataWithBuffer(InputStream inputStream, byte[] copyBuffer, DataListener listener)
			throws IOException
	{
		exhaustiveCopyDataWithBuffer(-1, inputStream, -1, null, copyBuffer, null);
	}


	public static void readDataWithBuffer(long dataSize, InputStream inputStream, byte[] copyBuffer,
			DataListener listener) throws IOException
	{
		exhaustiveCopyDataWithBuffer(dataSize, inputStream, -1, null, copyBuffer, listener);
	}

	public static void readDataWithBuffer(InputStream inputStream, OutputStream outputStream, byte[] copyBuffer)
			throws IOException
	{
		exhaustiveCopyDataWithBuffer(-1, inputStream, -1, outputStream, copyBuffer, null);
	}

	public static void readDataWithBuffer(long dataSize, InputStream inputStream, OutputStream outputStream,
			byte[] copyBuffer) throws IOException
	{
		exhaustiveCopyDataWithBuffer(dataSize, inputStream, -1, outputStream, copyBuffer, null);
	}

	public static void exhaustiveCopyDataWithBuffer(long dataSize, InputStream inputStream, long maxWriteAmount,
			OutputStream outputStream, byte[] copyBuffer) throws IOException
	{
		exhaustiveCopyDataWithBuffer(dataSize, inputStream, maxWriteAmount, outputStream, copyBuffer, null);
	}

	/**
	 * Copies up to maxWriteAmount bytes into outputStream, before reaches EOF in the inputStream.
	 * If dataSize > maxWriteAmount and there is still data available in the inputStream, reads the 
	 * the remaining (dataSize - maxWriteAmount) bytes without writing them to the outputStream.
	 */
	public static void exhaustiveCopyDataWithBuffer(final long dataSize, InputStream inputStream,
			final long maxWriteAmount, OutputStream outputStream, byte[] copyBuffer, DataListener listener)
			throws IOException
	{
		if (inputStream == null)
			return;

		int bufferSize = copyBuffer.length;

		long totalReadCount = 0;
		long remainingSize = dataSize;
		long remainingSpace = maxWriteAmount;

		int length = 0;
		int readSize = 0;
		int writeLength = 0;

		boolean eof = false;


		/*
		 * We need to read all the data from the stream, even if there is not much space to store it,
		 * otherwise subsequent responses will fail.
		 */
		//		boolean printBytes = true;
		//		byte[] printByteArray = new byte[16];
		while (!eof && (remainingSize > 0 || remainingSize == -1))
		{
			length = remainingSize > 0 ? (remainingSize < (long)bufferSize ? (int)remainingSize : bufferSize)
					: bufferSize;
			readSize = 0;
			while (readSize < length)
			{
				int readByteCount = 0;
				//				try
				//				{
				readByteCount = inputStream.read(copyBuffer, 0, length - readSize);
				if (listener != null && readByteCount >= 0)
				{
					if (listener.transfer(totalReadCount, copyBuffer, readByteCount))
						return;
				}
				totalReadCount += readByteCount;
				//					if(printBytes)
				//					{
				//						System.arraycopy(copyBuffer, 0, printByteArray, 0, readByteCount > 16 ? 16 : readByteCount);
				//						System.out.println("Read bytes from stream: " + Arrays.toString(printByteArray));
				//						printBytes = false;
				//					}

				//					getDataOutputStream().write(copyBuffer, 0, readByteCount);
				//					getDataOutputStream().flush();

				//				}
				//				catch(IndexOutOfBoundsException e)
				//				{
				//					logger.warning("loop:: dataSize = " + dataSize + "; maxWriteAmount = " + maxWriteAmount + "; readSize = " + readSize+ "; remainingSize = " + remainingSize + "; remainingSpace = " + remainingSpace + "; eof = " + eof);
				//				}

				if (readByteCount == -1)
				{
					eof = true;
					break;
				}
				readSize += readByteCount;
				//				logger.warning("loop:: dataSize = " + dataSize + "; maxWriteAmount = " + maxWriteAmount + "; readSize = " + readSize+ "; remainingSize = " + remainingSize + "; remainingSpace = " + remainingSpace + "; eof = " + eof);

				if (dataSize > 0)
					remainingSize -= readByteCount;

				if (outputStream != null)
				{

					if (remainingSpace == -1)
					{
						writeLength = readByteCount;
						outputStream.write(copyBuffer, 0, writeLength);
					}
					else
					{
						writeLength = remainingSpace > (long)readByteCount ? readByteCount : (int)remainingSpace;
						if (remainingSpace > 0)
						{
							outputStream.write(copyBuffer, 0, writeLength);
							remainingSpace -= writeLength;
						}
					}
				}
			}


		}
		//		logger.warning("dataSize = " + dataSize + "; maxWriteAmount = " + maxWriteAmount + "; readSize = " + readSize+ "; remainingSize = " + remainingSize + "; remainingSpace = " + remainingSpace + "; eof = " + eof);
		//		if(remainingSpace == 1022)
		//		{
		//			logger.warning("Invalid");
		//		}
		//		
	}


	/*
	 *	Number to/from Byte manipulation methods 
	 */

	public static void writeShort(OutputStream os, short value) throws IOException
	{
		os.write((byte)((value >>> 8) & 0xFF));
		os.write((byte)((value >>> 0) & 0xFF));
	}

	public static void writeInt(OutputStream os, int value) throws IOException
	{
		os.write((byte)((value >>> 24) & 0xFF));
		os.write((byte)((value >>> 16) & 0xFF));
		os.write((byte)((value >>> 8) & 0xFF));
		os.write((byte)((value >>> 0) & 0xFF));
	}

	public static void writeLong(OutputStream os, long value) throws IOException
	{
		os.write((byte)((value >>> 56) & 0xFF));
		os.write((byte)((value >>> 48) & 0xFF));
		os.write((byte)((value >>> 40) & 0xFF));
		os.write((byte)((value >>> 32) & 0xFF));
		os.write((byte)((value >>> 24) & 0xFF));
		os.write((byte)((value >>> 16) & 0xFF));
		os.write((byte)((value >>> 8) & 0xFF));
		os.write((byte)((value >>> 0) & 0xFF));
	}

	public static short readShort(InputStream is) throws IOException
	{
		return (short)(((is.read() & 0xFF) << 8) + ((is.read() & 0xFF) << 0));
	}

	public static int readInt(InputStream is) throws IOException
	{
		return (int)(((is.read() & 0xFF) << 24) + ((is.read() & 0xFF) << 16))
				+ (((is.read() & 0xFF) << 8) + ((is.read() & 0xFF) << 0));
	}

	public static long readLong(InputStream is) throws IOException
	{
		return (((long)(is.read() & 0xFF) << 56) + ((long)((is.read() & 0xFF)) << 48)
				+ ((long)((is.read() & 0xFF) & 0xFF) << 40) + ((long)(is.read() & 0xFF) << 32)
				+ ((long)(is.read() & 0xFF) << 24) + ((is.read() & 0xFF) << 16) + ((is.read() & 0xFF) << 8) + ((is
				.read() & 0xFF) << 0));
	}

	
	/*
	 *	Byte Array to/from Byte manipulation methods 
	 */

	public static void writeByteArray(OutputStream os, byte[] byteArray) throws IOException
	{
		int length = byteArray == null ? 0 : byteArray.length;
		writeInt(os, length);
		if(length > 0)
		{
			os.write(byteArray);
		}
	}

	public static void readByteArray(InputStream is, byte[] byteArray) throws IOException
	{
		int length = readInt(is);
		for(int i = 0; i < length; ++i)
		{
			byteArray[i] = (byte)is.read();
		}
	}
	
	/*
	 *	Bean to/from Byte manipulation methods 
	 */


	public static Object readBeanFromStreamAndClose(InputStream is) throws IOException, ClassNotFoundException
	{
		return readBeanFromStreamAndClose(is, null, false);
	}

	public static Object readBeanFromStreamAndClose(InputStream is, boolean base64decode) throws IOException, ClassNotFoundException
	{
		return readBeanFromStreamAndClose(is, base64decode);
	}
	
	public static Object readBeanFromStreamAndClose(InputStream is, Cipher cipher, boolean base64decode) throws IOException, ClassNotFoundException
	{
		ObjectInputStream objectStream = null;

		if(cipher != null && base64decode)
		{
			objectStream = new ObjectInputStream(new GZIPInputStream(new CipherInputStream(new Base64InputStream(is), cipher)));
		}
		else if(cipher != null )
		{
			objectStream = new ObjectInputStream(new GZIPInputStream(new CipherInputStream(is, cipher)));
		}
		else if(base64decode)
		{
			objectStream = new ObjectInputStream(new GZIPInputStream(new Base64InputStream(is)));
		}
		else
		{
			objectStream = new ObjectInputStream(new GZIPInputStream(is));
		}

		Object objectBean = objectStream.readObject();
		
		objectStream.close();
		
		return objectBean;
	}
	

	/*
	 * Writing several beans one after another turns out to be faulty, thus it is safer to write only a single bean at
	 * a time and close the stream.  
	 */
	public static void writeBeanToStreamAndClose(Object objectBean, OutputStream os) throws IOException, ClassNotFoundException
	{
		writeBeanToStreamAndClose(objectBean, os, null, false);
	}

	public static void writeBeanToStreamAndClose(Object objectBean, OutputStream os, boolean base64encode) throws IOException, ClassNotFoundException
	{
		writeBeanToStreamAndClose(objectBean, os, null, base64encode);
	}
	
	public static void writeBeanToStreamAndClose(Object objectBean, OutputStream os, Cipher cipher, boolean base64encode) throws IOException, ClassNotFoundException
	{
		ObjectOutputStream objectStream = null;
		
		if(cipher != null && base64encode)
		{
			objectStream = new ObjectOutputStream(new GZIPOutputStream(new CipherOutputStream(new Base64OutputStream(os, false), cipher)));
		}
		else if(cipher != null)
		{
			/*
			 * Nesting the CipherStream this way allows writing multiple objects with different ciphers
			 */
			objectStream = new ObjectOutputStream(new GZIPOutputStream(new CipherOutputStream(os, cipher)));
		}
		else if(base64encode)
		{
			objectStream = new ObjectOutputStream(new GZIPOutputStream(new Base64OutputStream(os, false)));
		}
		else
		{
			objectStream = new ObjectOutputStream(new GZIPOutputStream(os));
		}
		objectStream.writeObject(objectBean);
		objectStream.close();
		
		os.flush();
	}


//	public static void writeEncryptedDataAndClose(InputStream is, OutputStream os, Cipher cipher, byte[] buffer) throws IOException
//	{
//		writ
//	}
	
}
